package com.xuecheng.media.service.impl;

import com.adan.base.execption.XcPlusException;
import com.adan.base.model.PageParams;
import com.adan.base.model.PageResult;
import com.adan.base.model.RestResponse;
import com.alibaba.nacos.common.utils.IoUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import com.xuecheng.media.service.MediaProcessService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Slf4j
@Service
public class MediaFileServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFileService {

    @Autowired
    MediaFilesMapper mediaFilesMapper;
    @Resource
    private MinioClient minioClient;
    @Resource
    private MediaFileService mediaFileService;
    @Resource
    private MediaProcessService mediaProcessService;


    @Value("${minio.bucket.files}")
    private String bucket_files;

    @Value("${minio.bucket.videofiles}")
    private String bucket_video;


    @Override
    public PageResult<MediaFiles> queryMediaFiles(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

        // 构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        String filename = queryMediaParamsDto.getFilename();
        String fileType = queryMediaParamsDto.getFileType();
        queryWrapper.like(!StringUtils.isEmpty(filename), MediaFiles::getFilename, filename);
        queryWrapper.eq(!StringUtils.isEmpty(fileType), MediaFiles::getFileType, fileType);

        // 分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(
                list, total,
                pageParams.getPageNo(),
                pageParams.getPageSize()
        );
        return mediaListResult;

    }

    /**
     * putObject上传文件
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 文件信息
     * @param bytes               文件字节数组
     * @param folder              桶下边的子目录
     * @param objectName          对象名称
     * @return
     */
    @Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
        // 上传图片文件使用putObject
        String fileMD5 = DigestUtils.md5Hex(bytes);
        if (StringUtils.isEmpty(folder)) {
            // 文件目录不存在，自动生成目录
            folder = getFileFolder(true, true, true);
        } else if (!folder.endsWith("/")) {
            // 如果目录末尾没有 / ,则添加/
            folder = folder + "/";
        }
        // 判断文件名
        if (StringUtils.isEmpty(objectName)) {
            // 如果文件名为空，则设置其默认文件名为文件的md5码 + 文件后缀名
            String filename = uploadFileParamsDto.getFilename();
            objectName = fileMD5 + filename.substring(filename.lastIndexOf("."));
        }
        // 文件存储地址
        objectName = folder.concat(objectName);

        try {
            // 1.上传文件到minio
            uploadFileToMinio(bytes, objectName, bucket_files);
            // 2.保持到数据库
            MediaFiles mediaFile = mediaFileService.saveFileToDB(companyId, uploadFileParamsDto, objectName, fileMD5, bucket_files);
            // 3.封装返回数据
            UploadFileResultDto resultDto = new UploadFileResultDto();
            BeanUtils.copyProperties(mediaFile, resultDto);
            return resultDto;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传过程中出错", e.getMessage());
            XcPlusException.cast("上传过程中出错");
        }
        return null;
    }


    /**
     * 保存文件到数据库
     *
     * @param companyId           机构id
     * @param uploadFileParamsDto 上传文件的信息
     * @param objectName          对象名称
     * @param fileMD5             文件的md5码
     * @param bucket              桶
     * @return MediaFiles
     */
    @Transactional
    public MediaFiles saveFileToDB(Long companyId, UploadFileParamsDto uploadFileParamsDto, String objectName, String fileMD5, String bucket) {
        MediaFiles mediaFile = this.getById(fileMD5);
        if (mediaFile != null){
            return mediaFile;
        }
        mediaFile = new MediaFiles();
        BeanUtils.copyProperties(uploadFileParamsDto, mediaFile);
        mediaFile.setId(fileMD5);
        mediaFile.setFileId(fileMD5);
        mediaFile.setCompanyId(companyId);
        mediaFile.setBucket(bucket);
        mediaFile.setCreateDate(LocalDateTime.now());
        mediaFile.setStatus("1");
        mediaFile.setFilePath(objectName);
        // 获取源文件名的contentType
        String mimeType = getMimeType(objectName);
        // 如果是图片格式或者mp4格式，则设置URL属性，否则不设置
        if (mimeType.contains("image") || mimeType.contains("mp4")) {
            mediaFile.setUrl("/" + bucket + "/" + objectName);
        }
        // 查阅数据字典，002003表示审核通过
        mediaFile.setAuditStatus("002003");
        boolean save = this.save(mediaFile);
        if (!save) {
            XcPlusException.cast("保存文件信息失败");
        }

        // 添加到待处理任务表
        if ("video/x-msvideo".equals(mimeType)){
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFile, mediaProcess);
            mediaProcess.setStatus("1");//未处理
            mediaProcess.setFailCount(0);//失败次数默认为0
            boolean save1 = mediaProcessService.save(mediaProcess);
            if (!save1) {
                XcPlusException.cast("保存文件处理信息失败");
            }
        }
        return mediaFile;
    }

    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        // 1.首先判断数据库中是否存在该文件
        // 查询文件信息
        MediaFiles mediaFile = this.getById(fileMd5);
        if (mediaFile == null) {
            return RestResponse.success(false);
        }
        // 若数据库中存在，根据数据库中的文件信息，则继续判断minio中是否存在
        // 2.其次判断minio的bucket中是否存在该文件
        String bucket = mediaFile.getBucket();  // 桶
        String filePath = mediaFile.getFilePath();  // 存储目录
        // 判断文件是否在minio存在
        boolean flag = checkMinioFile(bucket, filePath);
        if (flag) {
            return RestResponse.success(true);
        }
        return RestResponse.success(false);
    }

    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        // 分块是否存在，只需要判断minio对应的目录下是否存在分块文件
        // 获取得到分块文件所在目录。“abcde”->“a/b/abcde”
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunkIndex;
        boolean flag = checkMinioFile(bucket_video, chunkFilePath);
        if (flag) {
            return RestResponse.success(true);
        }
        return RestResponse.success(false);
    }

    /**
     * 上传分块文件
     *
     * @param fileMd5            文件MD5
     * @param chunk              分块序号
     * @param localChunkFilePath 本地文件绝对路径
     * @return
     */
    @Override
    public boolean uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        // 获取文件类型
        String mimeType = getMimeType(null);
        // 获取文件在minio的存储路径“abcde”->“a/b/abcde”
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        String chunkFilePath = chunkFileFolderPath + chunk;
        // 上传文件到minio
        boolean flag = uploadFileToMinio(bucket_video, mimeType, chunkFilePath, localChunkFilePath);
        if (!flag) {
            log.error("上传分块文件失败:{}", chunkFileFolderPath);
            return false;
        }
        log.info("上传分块文件成功:{}", chunkFileFolderPath);
        return true;
    }

    /**
     * 合并分块接口实现
     * @param companyId           机构 id
     * @param fileMd5             文件 md5
     * @param chunkTotal          分块总和
     * @param uploadFileParamsDto 文件信息
     * @return
     */
    @Override
    public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        // 1.下载分块文件
        // 获取minio分块文件的路径
        String chunkFilePath = getChunkFileFolderPath(fileMd5);
        //获取文件类型mimeType
        String mimeType = getMimeType(null);
        // =================1. 合并最后一个文件操作===============
        // 将最后一个分块文件合并到上一个分块文件中
        try {
            mergeLastChunkFile(bucket_video, chunkFilePath, mimeType, chunkTotal - 1);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("合并最后一个分块文件失败");
            XcPlusException.cast("合并最后一个分块文件失败");
        }
        // =================2. 合并整个文件操作===============
        // 文件名称
        String filename = uploadFileParamsDto.getFilename();
        // 文件扩展名
        String extension = filename.substring(filename.lastIndexOf("."));
        //合并文件路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extension);
        // 合并分块文件
        mergeChunkFile(bucket_video, chunkFilePath, mergeFilePath, chunkTotal-1);
        //  =================3. 验证文件===============
        // 验证md5合并后的文件和源文件是否一致，从而判断是否上传成功
        File file = downloadFileFromMinIO(bucket_video, mergeFilePath);
        if (file == null){
            log.error("下载合并后文件失败,mergeFilePath:{}",mergeFilePath);
            return RestResponse.validfail("下载合并后文件失败。", false);
        }
        try {
            // 校验文件md5
            FileInputStream fileInputStream = new FileInputStream(file);
            // minio上文件的md5值
            String md5Hex = DigestUtils.md5Hex(fileInputStream);
            //比较md5值，不一致则说明文件不完整
            if (!fileMd5.equals(md5Hex)){
                return RestResponse.validfail("文件合并校验失败", false);
            }
            // 设置文件大小
            uploadFileParamsDto.setFileSize(file.length());
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (file != null){
                file.delete();
            }
        }
        //  =================4.保存文件信息===============
        mediaFileService.saveFileToDB(companyId, uploadFileParamsDto, mergeFilePath, fileMd5, bucket_video);
        //  =================5. 删除分块文件===============
        clearChunkFiles(bucket_video,chunkFilePath,chunkTotal-1);
        return RestResponse.success(true);
    }

    @Override
    public MediaFiles getPlayUrlByMediaId(String mediaId) {
        MediaFiles mediaFile = this.getById(mediaId);
        if (mediaFile == null || StringUtils.isEmpty(mediaFile.getUrl())){
            log.error("预览文件未解码,文件id：{}", mediaId);
            XcPlusException.cast("预览文件未解码");
        }
        return mediaFile;
    }

    /**
     * 删除分块文件
     * @param chunkFilePath 分块文件路径
     * @param chunkTotal 分块数量
     */
    private void clearChunkFiles(String bucket, String chunkFilePath, int chunkTotal) {
        Iterable<DeleteObject> object = Stream.iterate(0, i -> ++i).limit(chunkTotal)
                .map(i -> new DeleteObject(chunkFilePath.concat(Integer.toString(i))))
                .collect(Collectors.toList());
        RemoveObjectsArgs args = RemoveObjectsArgs.builder()
                .bucket(bucket)
                .objects(object)
                .build();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(args);
        for (Result<DeleteError> result : results) {
            try {
                DeleteError error = result.get();
                log.error( "清楚分块文件出错" + error.objectName() + "; " + error.message());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    /**
     * 从minio下载文件
     * @param bucket 桶
     * @param filePath 对象名称
     * @return 下载后的文件
     */
    @Override
    public File downloadFileFromMinIO(String bucket, String filePath) {
        File file = null;
        FileOutputStream outputStream = null;

        try {
            InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(filePath)
                    .build());
            file = File.createTempFile("mino", "merge");
            outputStream = new FileOutputStream(file);
            IoUtils.copy(inputStream, outputStream);
            return file;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("下载文件失败");
        } finally {
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }


    /**
     * 合并分块文件
     * @param buket 桶
     * @param fileFolderPath 分块文件所在目录
     * @param mergeFilePath 合并文件路径
     * @param chunkTotal 分块总数
     * @return 合并成功true
     */
    private void mergeChunkFile(String buket, String fileFolderPath, String mergeFilePath, int chunkTotal){
        // Stream.iterate 是 Java 8 中的一个方法，用于创建一个无限流
        // 第一个元素是初始值，后续每个元素都是前一个元素加1
        // 需要注意的是，由于 Stream.iterate 创建的是无限流，因此需要通过 limit() 或其他终止操作来限制流的大小
        List<ComposeSource> sources = Stream.iterate(0, i -> ++i).limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                .bucket(buket)
                .object(fileFolderPath.concat(Integer.toString(i)))
                .build()
        ).collect(Collectors.toList());
        ComposeObjectArgs composeObject = ComposeObjectArgs.builder()
                .bucket(buket)
                .object(mergeFilePath)
                .sources(sources)
                .build();
        try {
            minioClient.composeObject(composeObject);
            log.info("合并文件成功");
        } catch (Exception e) {
            e.printStackTrace();
            log.error("合并文件失败");
            XcPlusException.cast("合并文件失败");
        }
    }


    /**
     * 将最后一个分块文件合并到上一个分块文件中
     * 使得合并后的文件大小大于等于5MB
     * @param buket 桶
     * @param fileFolderPath 文件所在目录
     * @param contentType 文件类型
     * @param chunk 分块数
     * @throws Exception 合并失败
     */
    private void mergeLastChunkFile(String buket, String fileFolderPath, String contentType, int chunk) throws Exception {
        // 最后一个文件路径
        String lastFilePath = fileFolderPath.concat(Integer.toString(chunk));
        // 上一个文件路径
        String preFilePath = fileFolderPath.concat(Integer.toString(chunk - 1));
        // 1.获取最后一个分块文件大小
        long lastFilesize = minioClient.statObject(StatObjectArgs.builder()
                .bucket(buket)
                .object(lastFilePath)
                .build()).size();
        // 1.1 获取上一个分块文件大小
        long preFilesize = minioClient.statObject(StatObjectArgs.builder()
                .bucket(buket)
                .object(preFilePath)
                .build()).size();
        // 2.获取合并后的分块文件大小，使得合并后的文件大小大于等于5242880字节
        long fileSize = lastFilesize + preFilesize;
        // 3.获取到最后一个分块文件的内容流
        InputStream lastInputStream = minioClient.getObject(GetObjectArgs.builder()
                .bucket(buket)
                .object(lastFilePath)
                .build());
        // 4.获取到前一个分块文件的内容流
        InputStream preInputStream = minioClient.getObject(GetObjectArgs.builder()
                .bucket(buket)
                .object(preFilePath)
                .build());
        // 5.合并文件流生成新的输入流
        SequenceInputStream inputStream = new SequenceInputStream(preInputStream, lastInputStream);
        // 6.上传新的文件到上一个分块文件
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .bucket(buket)
                .object(preFilePath)
                .stream(inputStream, fileSize, -1)
                .contentType(contentType)
                .build();
        minioClient.putObject(putObjectArgs);
        /*File tempFile = File.createTempFile("minio", "lastChunk");
        FileOutputStream outputStream = new FileOutputStream(tempFile);
        IoUtils.copy(inputStream, outputStream);
        // 使用uploadObject方法
        minioClient.uploadObject(UploadObjectArgs.builder()
                .bucket(buket)
                .object(preFilePath)
                .filename(tempFile.getAbsolutePath())
                .build());
        tempFile.delete();*/
        // 7. 删除最后一个分块文件
        minioClient.removeObject(RemoveObjectArgs.builder()
                .bucket(buket)
                .object(lastFilePath)
                .build());
    }

    /**
     * 上传分块文件到minio
     *
     * @param bucket              桶
     * @param mimeType            文件类型
     * @param chunkFileFolderPath 上传对象名
     * @param localChunkFilePath  本地文件地址
     * @return 上传成功true
     */
    @Override
    public boolean uploadFileToMinio(String bucket, String mimeType, String chunkFileFolderPath, String localChunkFilePath) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(chunkFileFolderPath)
                    .filename(localChunkFilePath)
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到 minio 成功,bucket:{},objectName:{}", bucket, chunkFileFolderPath);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件到 minio 出错,bucket:{},objectName:{},错误原因:{}", bucket, chunkFileFolderPath, e.getMessage(), e);
            XcPlusException.cast("上传文件到文件系统失败");
        }
        return false;
    }


    /**
     * 判断文件在minio是否存在
     *
     * @param bucket        桶
     * @param chunkFilePath 文件路径
     * @return 存在 true； 不存在false
     */
    private boolean checkMinioFile(String bucket, String chunkFilePath) {
        // 文件流
        InputStream fileInputStream = null;
        try {
            fileInputStream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(chunkFilePath)
                    .build());
            if (fileInputStream != null) {
                // 分块已存在
                return true;
            }
        } catch (Exception e) {
            // 文件不存在
            return false;
        }
        // 文件不存在
        return false;
    }

    /**
     * 生成分块文件的目录路径。“abcde”->“a/b/abcde”
     *
     * @param fileMd5 文件MD5
     * @return minio文件路径
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    /**
     * 上传文件到minio
     *
     * @param bytes      文件字节数组
     * @param objectName 存储桶里的对象名称。
     * @param bucket     桶对象
     */
    private void uploadFileToMinio(byte[] bytes, String objectName, String bucket) {
        // 默认contentType为未知二进制流
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        // 判断对象名是否包含 .
        if (objectName.contains(".")) {
            // 有,则划分出扩展名
            String extension = objectName.substring(objectName.lastIndexOf("."));
            // 根据扩展名得到contentType，如果为未知扩展名，例如 .abc之类的东西，则会返回null
            ContentInfo extensionInfo = ContentInfoUtil.findExtensionMatch(extension);
            if (extensionInfo != null) {
                contentType = extensionInfo.getMimeType();
            }
        }
        // 1.上传到minio
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .bucket(bucket)
                .object(objectName)     // 存储桶里的对象名称。
                .stream(inputStream, inputStream.available(), -1) // 要上传的流、大小
                .contentType(contentType)
                .build();
        try {
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            log.error("上传到文件系统出错:{}", e.getMessage());
            XcPlusException.cast("上传到文件系统出错");
        }
    }

    /**
     * 获取文件默认存储目录路径 年/月/日
     *
     * @param year  年
     * @param month 月
     * @param day   日
     * @return 目录
     */
    private String getFileFolder(boolean year, boolean month, boolean day) {
        StringBuffer stringBuffer = new StringBuffer();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String date = dateFormat.format(new Date());
        String[] split = date.split("-");
        String string = split.toString();
        if (year) {
            stringBuffer.append(split[0]).append("/");
        }
        if (month) {
            stringBuffer.append(split[1]).append("/");
        }
        if (day) {
            stringBuffer.append(split[2]).append("/");
        }
        return stringBuffer.toString();
    }

    /**
     * 获取文件默认存储目录路径 年/月/日
     */
    private String getFileFolder() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String date = dateFormat.format(new Date()).replace("-", "/") + "/";
        return date;
    }

    /**
     * 获取文件类型
     *
     * @param extension
     * @return
     */
    private String getMimeType(String extension) {
        if (extension == null) {
            extension = "";
        }
        if (extension.contains(".")){
            extension = extension.substring(extension.lastIndexOf("."));
        }
        // 通用mimeType，字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        ContentInfo contentInfo = ContentInfoUtil.findExtensionMatch(extension);
        if (contentInfo != null) {
            mimeType = contentInfo.getMimeType();
        }
        return mimeType;
    }
    /**
     * 得到合并后的文件的地址
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return
     */
    private String getFilePathByMd5(String fileMd5,String fileExt){
        return   fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
    }

}
